﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace System.Text.Parsing
{
    /// <summary>Represents a grammar containing one or more parser / tokenizer definitions.</summary>
    public sealed class Grammar
    {
        private readonly GrammarTokenizerCollection _tokenizers;    // The list of tokenizer definitions.

        //-----------------------------------------------------------------------
        //     Properties
        //-----------------------------------------------------------------------

        /// <summary>Gets the collection of tokenizers.</summary>
        public GrammarTokenizerCollection Tokenizers { get { return _tokenizers; } }

        //-----------------------------------------------------------------------
        //     Constructors
        //-----------------------------------------------------------------------

        /// <summary>Initializes an instance of the <see cref="Grammar"/> class.</summary>
        public Grammar()
        {
            _tokenizers = new GrammarTokenizerCollection();
        }

        //-----------------------------------------------------------------------
        //     Methods
        //-----------------------------------------------------------------------

        /// <summary>Parses a <see cref="Grammar"/> definition </summary>
        /// <param name="xmlReader">The <see cref="XmlReader"/> object to use.</param>
        /// <param name="documentFileName">The file name of the document.</param>
        /// <param name="startLocation">The location of the first character in the XML stream.</param>
        /// <returns>A <see cref="Grammar"/> object.</returns>
        public static Grammar FromXml(XmlReader xmlReader, string documentFileName, TextLocation startLocation)
        {
            if (xmlReader == null) throw new ArgumentNullException("xmlReader");
            var lineInfo = xmlReader as IXmlLineInfo;
            var grammar = new Grammar();

            // Make sure the document is not empty.
            if (xmlReader.MoveToContent() == XmlNodeType.None)
                throw XmlParsingException(lineInfo, "Document is empty");

            // Expect root node.
            if (xmlReader.Name != "Grammar")
                throw XmlParsingException(lineInfo, "Expecting root element Grammar");

            // Read content.
            ParseXmlGrammarContent(grammar, lineInfo, xmlReader, documentFileName, startLocation);

            // Okay.
            return grammar;
        }

        //-----------------------------------------------------------------------
        //     Non Public
        //-----------------------------------------------------------------------

        // Regex for identifiers.
        private static readonly Regex _identifierRegex = new Regex(@"^[\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Lm}_][\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nd}\p{Lm}_]*$", RegexOptions.Compiled);

        // ParseXmlGrammarContent method.
        private static void ParseXmlGrammarContent(Grammar grammar, IXmlLineInfo lineInfo, XmlReader xmlReader, string documentFileName, TextLocation startLocation)
        {
            // Read attributes.
            foreach (var xmlAttribute in Attributes(xmlReader))
                throw XmlParsingException(lineInfo, "Unexpected attribute {0}", xmlAttribute.Name);

            // For each element...
            foreach (var xmlElement in Elements(xmlReader))
                switch (xmlElement.Name)
                {
                    case "Tokenizer":
                        ParseXmlTokenizer(grammar, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    default:
                        throw XmlParsingException(lineInfo, "Unexpected element {0}", xmlElement.Name);
                }
        }

        // ParseXmlTokenizer method.
        private static void ParseXmlTokenizer(Grammar grammar, IXmlLineInfo lineInfo, XmlReader xmlReader, string documentFileName, TextLocation startLocation)
        {
            GrammarTextValue? name = null;
            GrammarTextValue? implementation = null;

            // Read attributes.
            foreach (var xmlAttribute in Attributes(xmlReader))
                switch (xmlAttribute.Name)
                {
                    case "name":
                        if (!_identifierRegex.IsMatch(xmlAttribute.Value))
                            throw XmlParsingException(lineInfo, "Value '{0}' is not a valid identifier", xmlAttribute.Value);
                        name = new GrammarTextValue(xmlAttribute.Value, SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));
                        break;
                    case "implementation":
                        if (!_identifierRegex.IsMatch(xmlAttribute.Value))
                            throw XmlParsingException(lineInfo, "Value '{0}' is not a valid identifier", xmlAttribute.Value);
                        implementation = new GrammarTextValue(xmlAttribute.Value, SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));
                        break;
                    default:
                        throw XmlParsingException(lineInfo, "Unexpected attribute {0}", xmlAttribute.Name);
                }

            // Check.
            if (!name.HasValue) throw XmlParsingException(lineInfo, "Missing 'name' attribute");
            if (!implementation.HasValue) throw XmlParsingException(lineInfo, "Missing 'implementation' attribute");

            // Create.
            var tokenizer = new GrammarTokenizer(name.Value, implementation.Value);
            grammar.Tokenizers.Add(tokenizer);

            // For each element...
            foreach (var xmlElement in Elements(xmlReader))
                switch (xmlElement.Name)
                {
                    case "Token":
                        ParseXmlToken(tokenizer, null, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    case "TokenGroup":
                        ParseXmlTokenGroup(tokenizer, null, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    case "Property":
                        ParseXmlProperty(tokenizer.Properties, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    case "CodeBlock":
                        ParseXmlCodeBlock(tokenizer.CodeBlocks, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    default:
                        throw XmlParsingException(lineInfo, "Unexpected element {0}", xmlElement.Name);
                }
        }

        // ParseXmlTokenGroup method.
        private static void ParseXmlTokenGroup(GrammarTokenizer tokenizer, GrammarTokenGroup parentGroup, IXmlLineInfo lineInfo, XmlReader xmlReader, string documentFileName, TextLocation startLocation)
        {
            GrammarTextValue? name = null;
            var sourceInfo = SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo);

            // Read attributes.
            foreach (var xmlAttribute in Attributes(xmlReader))
                switch (xmlAttribute.Name)
                {
                    case "name":
                        if (!_identifierRegex.IsMatch(xmlAttribute.Value))
                            throw XmlParsingException(lineInfo, "Value '{0}' is not a valid identifier", xmlAttribute.Value);
                        name = new GrammarTextValue(xmlAttribute.Value, SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));
                        break;
                    default:
                        throw XmlParsingException(lineInfo, "Unexpected attribute {0}", xmlAttribute.Name);
                }

            // Create group and add it.
            var group = new GrammarTokenGroup(name.HasValue ? name.Value : GrammarTextValue.Empty, parentGroup, sourceInfo);
            tokenizer.TokenGroups.Add(group);

            // For each element...
            foreach (var xmlElement in Elements(xmlReader))
                switch (xmlElement.Name)
                {
                    case "Token":
                        ParseXmlToken(tokenizer, group, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    case "Property":
                        ParseXmlProperty(group.Properties, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    case "CodeBlock":
                        ParseXmlCodeBlock(group.CodeBlocks, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    case "TokenGroup":
                        ParseXmlTokenGroup(tokenizer, group, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    default:
                        throw XmlParsingException(lineInfo, "Unexpected element {0}", xmlElement.Name);
                }
        }

        // ParseXmlToken method.
        private static void ParseXmlToken(GrammarTokenizer tokenizer, GrammarTokenGroup group, IXmlLineInfo lineInfo, XmlReader xmlReader, string documentFileName, TextLocation startLocation)
        {
            GrammarTextValue? name = null;
            GrammarTextValue? definition = null;
            var sourceInfo = SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo);

            // Read attributes.
            foreach (var xmlAttribute in Attributes(xmlReader))
                switch (xmlAttribute.Name)
                {
                    case "name":
                        if (!_identifierRegex.IsMatch(xmlAttribute.Value))
                            throw XmlParsingException(lineInfo, "Value '{0}' is not a valid identifier", xmlAttribute.Value);
                        name = new GrammarTextValue(xmlAttribute.Value, SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));
                        break;
                    case "definition":
                        definition = new GrammarTextValue(xmlAttribute.Value.Trim(), SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));
                        break;
                    default:
                        throw XmlParsingException(lineInfo, "Unexpected attribute {0}", xmlAttribute.Name);
                }

            // Check.
            if (!name.HasValue) throw XmlParsingException(lineInfo, "Missing 'name' attribute");
            if (tokenizer.Tokens.Contains(name.Value.Text))
                throw XmlParsingException(lineInfo, "A token named {0} already exists", name.Value.Text);

            // Create collections.
            var properties = new GrammarPropertyCollection();
            var codeBlocks = new GrammarCodeBlockCollection();

            // For each element...
            foreach (var xmlElement in Elements(xmlReader))
                switch (xmlElement.Name)
                {
                    case "Definition":
                        if (definition.HasValue)
                            throw XmlParsingException(lineInfo, "Token definition already specified");
                        definition = new GrammarTextValue(xmlReader.ReadElementContentAsString().Trim(), SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));
                        break;
                    case "Property":
                        ParseXmlProperty(properties, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    case "CodeBlock":
                        ParseXmlCodeBlock(codeBlocks, lineInfo, xmlElement, documentFileName, startLocation);
                        break;
                    default:
                        throw XmlParsingException(lineInfo, "Unexpected element {0}", xmlElement.Name);
                }

            // Add token to list.
            tokenizer.Tokens.Add(new GrammarToken(name.Value, definition.Value, group, sourceInfo, properties, codeBlocks));
        }

        // ParseXmlProperty method.
        private static void ParseXmlProperty(GrammarPropertyCollection grammarPropertyCollection, IXmlLineInfo lineInfo, XmlReader xmlReader, string documentFileName, TextLocation startLocation)
        {
            GrammarTextValue? name = null;
            var sourceInfo = SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo);

            // Read attributes.
            foreach (var xmlAttribute in Attributes(xmlReader))
                switch (xmlAttribute.Name)
                {
                    case "name":
                        if (!_identifierRegex.IsMatch(xmlAttribute.Value))
                            throw XmlParsingException(lineInfo, "Value '{0}' is not a valid identifier", xmlAttribute.Value);
                        name = new GrammarTextValue(xmlAttribute.Value, SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));
                        break;
                    default:
                        throw XmlParsingException(lineInfo, "Unexpected attribute {0}", xmlAttribute.Name);
                }

            // Check.
            if (!name.HasValue) throw XmlParsingException(lineInfo, "Missing 'name' attribute");

            // Read content.
            var value = xmlReader.IsEmptyElement ? GrammarTextValue.Empty : new GrammarTextValue(xmlReader.ReadElementContentAsString(), SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));

            // Add code block.
            grammarPropertyCollection.Add(new GrammarProperty(name.Value, value, sourceInfo));
        }

        // ParseXmlCodeBlock method.
        private static void ParseXmlCodeBlock(GrammarCodeBlockCollection grammarCodeBlockCollection, IXmlLineInfo lineInfo, XmlReader xmlReader, string documentFileName, TextLocation startLocation)
        {
            GrammarTextValue? name = null;
            var sourceInfo = SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo);

            // Read attributes.
            foreach (var xmlAttribute in Attributes(xmlReader))
                switch (xmlAttribute.Name)
                {
                    case "name":
                        if (!_identifierRegex.IsMatch(xmlAttribute.Value))
                            throw XmlParsingException(lineInfo, "Value '{0}' is not a valid identifier", xmlAttribute.Value);
                        name = new GrammarTextValue(xmlAttribute.Value, SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));
                        break;
                    default:
                        throw XmlParsingException(lineInfo, "Unexpected attribute {0}", xmlAttribute.Name);
                }

            // Check.
            if (!name.HasValue) throw XmlParsingException(lineInfo, "Missing 'name' attribute");

            // Read content.
            var code = new GrammarTextValue(xmlReader.ReadElementContentAsString().Trim(), SourceRangeFromXmlLineInfo(documentFileName, startLocation, lineInfo));

            // Add code block.
            grammarCodeBlockCollection.Add(new GrammarCodeBlock(name.Value, code, sourceInfo));
        }

        /*
        // ReadElementContent method.
        private static GrammarTextValue ReadElementContent(XmlReader xmlReader, string documentFileName, TextLocation startLocation, IXmlLineInfo lineInfo)
        {
            if (xmlReader.IsEmptyElement)
                return GrammarTextValue.Empty;
            else
            {
                switch (xmlReader.MoveToContent())
                {
                    case XmlNodeType.CDATA:
                        xmlReader.read
                    default:
                }
            }
            else throw new Xml
        }*/

        // SourceRangeFromXmlLineInfo method.
        private static GrammarSourceRange SourceRangeFromXmlLineInfo(string documentFileName, TextLocation startLocation, IXmlLineInfo lineInfo)
        {
            if (lineInfo != null)
            {
                var line = lineInfo.LineNumber;
                var pos = lineInfo.LinePosition;
                if (line != 0)
                {
                    if (line == 1)
                        pos += startLocation.ColumnNumber;
                    line += startLocation.LineNumber - 1;
                    return new GrammarSourceRange(documentFileName, new TextRange(new TextLocation(-1, line, pos), TextLocation.Unknown));
                }
            }
            return new GrammarSourceRange(documentFileName, new TextRange(startLocation, TextLocation.Unknown));
        }

        // XmlParsingException method.
        [DebuggerHidden]
        private static Exception XmlParsingException(IXmlLineInfo lineInfo, string format, params object[] args)
        {
            if (lineInfo != null)
                return new XmlException(String.Format(format, args), null, lineInfo.LineNumber, lineInfo.LinePosition);
            else
                return new XmlException(String.Format(format, args));
        }

        // Iterates through children nodes of current node.
        private static IEnumerable<XmlReader> Elements(XmlReader reader)
        {
            if (!reader.IsEmptyElement)
            {
                for (; ; )
                {
                    // Advances.
                    if (!reader.Read()) break;

                    // Move to content.
                    var nodeType = reader.MoveToContent();

                    // If we reached the end of the block...
                    if (nodeType == XmlNodeType.EndElement) break;
                    // If we reached a new element...
                    else if (nodeType == XmlNodeType.Element)
                    {
                        yield return reader;
                    }
                    // Otherwise go on.
                    else continue;
                }

                // Read again.
                reader.Read();
            }
        }

        // Iterates through attributes of current node.
        private static IEnumerable<XmlReader> Attributes(XmlReader reader)
        {
            if (reader.HasAttributes)
            {
                for (int i = 0; i < reader.AttributeCount; i++)
                {
                    // Advance.
                    reader.MoveToAttribute(i);

                    // Return.
                    yield return reader;
                }

                // Read again.
                reader.MoveToElement();
            }
        }
    }
}
